/*
 *  Copyright (C) 2004 Cidero, Inc.
 *
 *  Permission is hereby granted to any person obtaining a copy of 
 *  this software to use, copy, modify, merge, publish, and distribute
 *  the software for any non-commercial purpose, subject to the
 *  following conditions:
 *  
 *  The above copyright notice and this permission notice shall be included
 *  in all copies or substantial portions of the Software.
 *
 *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 
 *  OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 *  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 
 *  THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 *  LIABILITY IN CONNECTION WITH THE SOFTWARE.
 * 
 *  File: $RCSfile: ATronAVTransport.java,v $
 *
 */

package com.cidero.bridge.audiotron;

import java.util.Observable;
import java.util.Observer;
import java.util.logging.Logger;

import org.cybergarage.upnp.Action;
import org.cybergarage.upnp.StateVariable;
import org.cybergarage.upnp.device.InvalidDescriptionException;

import com.cidero.bridge.MediaRendererException;
import com.cidero.bridge.MediaRendererSession;
import com.cidero.upnp.AVTransport;

/**
 * AudioTron UPnP Bridge AVTransport Service class. 
 *
 */
public class ATronAVTransport extends AVTransport
                              implements Observer
{
  private static Logger logger = 
    Logger.getLogger("com.cidero.bridge.audiotron");

  ATronMediaRenderer       mediaRenderer;
  MediaRendererSession currentSession;
  
  PlayQueue playQueue;   // Cache for retrieved Audiotron play queue data

  /**
   * Constructor
   */
  public ATronAVTransport( ATronMediaRenderer mediaRenderer )
    throws InvalidDescriptionException
  {
    super( mediaRenderer );
    
    logger.fine("Entered ATronAVTransport constructor");

    this.mediaRenderer = mediaRenderer;

    mediaRenderer.getStateModel().addObserver(this);
    
    playQueue = new PlayQueue();

    initializeStateVariables();

    logger.fine("Leaving AVTransport constructor");
  }

  /**
   *  Initialize state variables. Note that required state variables 
   *  have been given 'reasonable' default values in the base class
   *  version of this routine 
   */
  public void initializeStateVariables()
  {
    //setStateVariable("TransportState", AVTransport.STOPPED );
  }
  
  /**
   * Start/Restart playback of the current session.  All renderers
   * attached to current session are started/restarted.
   *
   * @param  action  UPNP Action object
   */
  public boolean actionPlay( Action action )
  {
    String id = action.getArgumentValue("InstanceID");
    String speed = action.getArgumentValue("Speed");

    logger.fine("Play: Entered, instanceId = " + id );

    // Get currentURI from state variable. State variable gets set by 
    // SetAVTransportURI action method in base class
    StateVariable stateVar = getService().getStateVariable( "AVTransportURI" );
    String currentURI = stateVar.getValue();
    
    String currentURIMetaData = null;
    stateVar = getService().getStateVariable( "AVTransportURIMetaData" );
    if( stateVar != null )
      currentURIMetaData = stateVar.getValue();

    logger.fine("play - getting session for URI " + currentURI );

    try
    {
      //
      // Check to see if there is already an active session for the 
      // requested URI. If so, join the session. If not, create a 
      // new session.  If the session is stopped (paused), it is 
      // restarted (all listening renderers are restarted as well)
      //
      if( (currentSession = 
           MediaRendererSession.findSession( currentURI )) != null )
      {
        currentSession.addRendererBridge( mediaRenderer );  // Join
      }
      else
      {
        logger.fine("Play: No session found - creating new one" );
        currentSession = new MediaRendererSession( mediaRenderer,
                                                   currentURI,
                                                   currentURIMetaData,
                                                   null,  // userAgent
                                                   0 );   // syncWaitMs
      }
    
      // Start device, and session if it's not already running on 
      // behalf of another device
      logger.fine("Starting session with resourceURL: " + 
                  currentSession.getResourceURL() );

      currentSession.setPlaySpeed( action.getArgumentValue("Speed" ) );
      currentSession.start();

      return true;
    }
    catch( Exception e )
    {
      logger.warning("Exception processing play action:" + e );
      return false;
    }

  }


  public boolean actionPause( Action action )
  {
    String id = action.getArgumentValue("InstanceID");

    logger.fine("Pause: Entered, instanceID = " + id );

    //    if( currentSession != null )
    //      currentSession.pause();

    //updateStateVariable( "TransportState", STATE_PAUSED_PLAYBACK );

    return true;
  }

  public boolean actionStop( Action action )
  {
    String id = action.getArgumentValue("InstanceID");

    logger.fine("Stop: Entered, instanceId = " + id );

    // Shut down most recently queued session, if it is currently running
    if( currentSession == null )
    {
      logger.fine("Stop: No active rendering session - ignoring action" );
      return false;
    }
        
    logger.fine("Stopping session for URL: " + 
                       currentSession.getResourceURL() );

    //
    // Stop session - all renderers associated with session are stopped.
    // (TODO: current design - may want way to stop just this renderer
    //
    currentSession.stop();
    currentSession = null;
    
    return true;
  }

  public boolean actionSeek( Action action )
  {
    String id = action.getArgumentValue("InstanceID");

    logger.fine("Seek: Entered, instanceID = " + id );

    return false;
  }

  public boolean actionNext( Action action )
  {
    logger.fine("Next: Entered" );
    return false;
  }

  public boolean actionPrevious( Action action )
  {
    logger.fine("Previous: Entered" );
    return false;
  }

  public boolean actionSetPlayMode( Action action )
  {
    logger.fine("SetPlayMode: Entered" );

    String id = action.getArgumentValue("InstanceID");
    String playMode = action.getArgumentValue("NewPlayMode");

    updateStateVariable( "CurrPlayMode", playMode );
    
    return false;
  }
  
  //-------------------------------------------------------------------
  // Following methods exist as separate entities from the actionXXX() 
  // methods since the actual startup of playbacks are controlled via
  // a renderer session object (to allow for synchronized playbacks
  // using multiple devices)    
  //
  // Example of control flow for 'play' action:
  //
  //   actionPlay() -> session.start() -> ATronAVTransport.play()
  //-------------------------------------------------------------------

  /**
   *  Set the playlist queue up and position to the right item. Note 
   *  that this call is invoked by the renderer session, and uses the
   *  URI of the HTTP server proxy that is built in to the renderer
   *  bridge.  The URI used contains enough info so that when the 
   *  HTTP server proxy sees it, it knows device type, etc...
   *  For example, for the Audiotron, the URL will be something like:
   *  
   *    http://l92.168.1.100:8081/Audiotron/LivingRoom
   */
  public void setTransportURI( String uri ) throws MediaRendererException
  {
    // For now, stop ATron prior to messing with play queue. It seems to 
    // sometimes get confused otherwise.
    stop();
    clearPlayQueue();
    
    String response;

    // Add all items in 'Home' category to queue
    response = mediaRenderer.send( "/apiqfile.asp?type=web&file=Home",
                                      ATronMediaRenderer.DEFAULT_TIMEOUT );

    // Only need to actually get the play queue once (shouldn't change 
    // following user's initial setup)
    if( ! playQueue.isValid() )
    {
      response = mediaRenderer.send( "/apigetinfo.asp?type=playq",
                                     ATronMediaRenderer.DEFAULT_TIMEOUT );
      logger.fine("Get playQ response: " + response );

      if( ! playQueue.parse( response ) )
      {
        throw new MediaRendererException( "Audiotron play queue syntax error");
      }
        
    }
    
    // Get playlist index (song #) of requested URI.  Error if not 
    // found 
    int songNumber = playQueue.getSongNumber( uri );
    if( songNumber < 0 )
      throw new MediaRendererException( "Audiotron can't find bridge URI" );
    
    // Position to desired item
    response = mediaRenderer.send( "/apicmd.asp?cmd=goto&arg=" + 
                                   songNumber,
                                   ATronMediaRenderer.DEFAULT_TIMEOUT );
    if( response == null )
      throw new MediaRendererException( "goto timeout" );
  }

  /**
   * Clear the play queue
   */
  public void clearPlayQueue()
  {
    String response = mediaRenderer.send( "/apicmd.asp?cmd=clear",
                                         ATronMediaRenderer.DEFAULT_TIMEOUT );
  }

  /** 
   * Start playback.
   *
   * Audiotron will not provide HTTP response until it actually gets
   * enough data to fill it's input buffer. To allow this routine to
   * return immediately after issuing a play cmd (the desired behavior
   * from the caller's perspective), a separate thread is used to
   * issue the command asynchronously
   *
   * @param speed    (Ignored by this driver)
   */
  public void play( String speed ) throws MediaRendererException
  {
    mediaRenderer.send( "/apicmd.asp?cmd=play", 0 );

    //if( response == null )
    //    throw new MediaRendererException( "pause: timeout");

    // Assume success - update state variable. If play fails, state will
    // be properly reset by status monitoring logic
    updateStateVariable("TransportState", AVTransport.STATE_PLAYING );

  }

  public synchronized void pause() throws MediaRendererException
  {
    String response = mediaRenderer.send( "/apicmd.asp?cmd=pause",
                                         ATronMediaRenderer.DEFAULT_TIMEOUT );
    if( response == null )
      throw new MediaRendererException( "pause: timeout");

    // Success - update state variable
    updateStateVariable("TransportState", AVTransport.STATE_PAUSED_PLAYBACK );
  }

  public synchronized void stop() throws MediaRendererException
  {
    String response = mediaRenderer.send( "/apicmd.asp?cmd=stop",
                                         ATronMediaRenderer.DEFAULT_TIMEOUT );
    if( response == null )
      throw new MediaRendererException( "stop: timeout");

    // Success - update state variable
    updateStateVariable("TransportState", AVTransport.STATE_STOPPED );
  }

  /**
   *  RenderingControl is an observer of ATronStateModel so that it can
   *  pick up state changes made using the ATron front panel or remote.
   *  If the ATronStateModel info differs from the UPnP state variables,
   *  update the state variables and fire off a LastChange event
   */
  public void update( Observable model, Object arg )
  {
    ATronStateModel stateModel = (ATronStateModel)model;

    //
    // Update the UPnP state variables to match the state model
    // Right now the only state being retrived from ATron is TransportState
    //
    updateStateVariable( "TransportState", stateModel.getTransportState() );
  }

  /**
   * Get information associated with the current position of a transport 
   * instance 
   *
   * Required Action: Yes
   */
  /*
  public boolean actionGetPositionInfo( Action action )
  {
    String instanceID = action.getArgumentValue("InstanceID");

    logger.finest("GetPositionInfo: Entered, instanceId = " +
                  instanceID );

    action.setArgumentValue("Track", "1" );
    action.setArgumentValue("TrackDuration", "00:03:20" );
    action.setArgumentValue("TrackMetaData", "" );
    action.setArgumentValue("TrackURI", "" );

    long relTimeSecs = (System.currentTimeMillis() - startTimeMillis)/1000;

    // Don't bother with Date/Calendar class here (overhead)
    long hours = relTimeSecs/3600;
    relTimeSecs -= hours*3600;
    long minutes = relTimeSecs/60;
    long seconds = relTimeSecs - minutes*60;
    
    NumberFormat nf = NumberFormat.getInstance();
    nf.setMinimumIntegerDigits(2);
    nf.setGroupingUsed(false);
    
    String relTime = nf.format(hours) + ":" + nf.format(minutes) + ":" +
                     nf.format(seconds);
    
    action.setArgumentValue("RelTime", relTime );
    action.setArgumentValue("AbsTime", AVTransport.NOT_IMPLEMENTED );

    // TODO: Spec says following need to be set to 'max value of i4 data type
    // to signify "NOT_IMPLEMENTED" 
    action.setArgumentValue("RelCount", AVTransport.NOT_IMPLEMENTED_I4 );
    action.setArgumentValue("AbsCount", "0" );

    return true;
  }
  */

}

